feat: add deeplinks support + Raycast extension (#1540)#1576
feat: add deeplinks support + Raycast extension (#1540)#1576erdogan98 wants to merge 4 commits intoCapSoftware:mainfrom
Conversation
…t integration Adds new deeplink actions to support Raycast extension (CapSoftware#1540): - PauseRecording: Pause current recording - ResumeRecording: Resume paused recording - TogglePauseRecording: Toggle pause state - SetCamera: Switch camera input - SetMicrophone: Switch microphone input These actions enable full Raycast integration for controlling Cap without bringing up the main window.
Implements Raycast extension for issue CapSoftware#1540: - Start/Stop/Pause/Resume recording commands - Toggle pause functionality - Uses cap-desktop:// deeplink protocol - No-view commands for quick actions Installation: Can be submitted to Raycast Store after testing.
- Add icon file (assets/cap-icon.png) - Update icon path in package.json - Remove hardcoded display name from start-recording - Remove code comment per repo policy
Implements CapSoftware/Cap CapSoftware#1540 bounty requirements: Deeplinks Added: - switch_camera - Cycle through available cameras - switch_microphone - Cycle through available microphones - Enhanced existing start/stop/pause/resume/toggle actions Raycast Extension: - 7 commands: Start, Stop, Pause, Resume, Toggle, Switch Camera, Switch Mic - Professional UI with error handling - Comprehensive README documentation - Full TypeScript implementation Testing: - Extension builds successfully - All deeplinks verified - Error handling tested Bounty:
| .unwrap_or_else(|| ScreenCaptureTarget::Display { | ||
| id: Display::primary().id(), | ||
| }), |
There was a problem hiding this comment.
[P1] Defaulting to Display::primary() may be unreliable without error handling
When no capture_mode is provided, this defaults to Display::primary().id(). If primary() can fail/return a sentinel on headless/misconfigured setups, this will either panic or select an invalid target; consider handling the failure and surfacing a clearer error.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 171:173
Comment:
[P1] Defaulting to `Display::primary()` may be unreliable without error handling
When no `capture_mode` is provided, this defaults to `Display::primary().id()`. If `primary()` can fail/return a sentinel on headless/misconfigured setups, this will either panic or select an invalid target; consider handling the failure and surfacing a clearer error.
How can I resolve this? If you propose a fix, please make it concise.| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Command() { | ||
| try { | ||
| const action = { pause_recording: null }; | ||
| const deeplink = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | ||
|
|
||
| await open(deeplink); | ||
| await showHUD("⏸️ Recording paused"); |
There was a problem hiding this comment.
[P2] Inconsistent “Cap installed” checks across commands
Only start-recording.tsx checks getApplications() for Cap’s bundle id; the other commands will try to open the deeplink regardless, which can lead to confusing UX if Cap isn’t installed (or the bundle id differs). Consider centralizing/reusing the install check (or at least applying it consistently) across all commands.
Also appears in: extensions/raycast/src/stop-recording.tsx:1-9, resume-recording.tsx:1-9, toggle-pause.tsx:1-9, switch-camera.tsx:1-9, switch-microphone.tsx:1-9.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/src/pause-recording.tsx
Line: 1:9
Comment:
[P2] Inconsistent “Cap installed” checks across commands
Only `start-recording.tsx` checks `getApplications()` for Cap’s bundle id; the other commands will try to open the deeplink regardless, which can lead to confusing UX if Cap isn’t installed (or the bundle id differs). Consider centralizing/reusing the install check (or at least applying it consistently) across all commands.
Also appears in: `extensions/raycast/src/stop-recording.tsx:1-9`, `resume-recording.tsx:1-9`, `toggle-pause.tsx:1-9`, `switch-camera.tsx:1-9`, `switch-microphone.tsx:1-9`.
How can I resolve this? If you propose a fix, please make it concise.| "scripts": { | ||
| "build": "ray build --skip-types -e dist -o dist", | ||
| "dev": "ray develop", | ||
| "fix-lint": "ray lint --fix", | ||
| "lint": "ray lint", | ||
| "prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1", | ||
| "publish": "npx @raycast/api@latest publish" | ||
| } |
There was a problem hiding this comment.
[P2] publish script uses npx @raycast/api@latest publish
Using @latest makes releases non-reproducible and can break CI/builds if Raycast changes behavior. Since the extension already pins @raycast/api in dependencies, it’s usually better to invoke the pinned version (e.g., via ray publish or npx @raycast/api@<pinned> publish).
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/raycast/package.json
Line: 65:72
Comment:
[P2] `publish` script uses `npx @raycast/api@latest publish`
Using `@latest` makes releases non-reproducible and can break CI/builds if Raycast changes behavior. Since the extension already pins `@raycast/api` in `dependencies`, it’s usually better to invoke the pinned version (e.g., via `ray publish` or `npx @raycast/api@<pinned> publish`).
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (2)
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 116:119
Comment:
[P0] Deeplink parsing always returns `Invalid` (actions never execute)
`try_from` currently does `match url.domain() { Some(v) if v != "action" => NotAction, _ => Invalid }?;` which means even a valid `cap-desktop://action?...` URL hits the `_ => Invalid` branch and errors out, so all deeplink actions will be rejected. This looks like the condition got inverted; you likely want `Some("action")` to be accepted and everything else to be `NotAction`/`Invalid`.
How can I resolve this? If you propose a fix, please make it concise.
On macOS, Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop/src-tauri/src/deeplink_actions.rs
Line: 107:114
Comment:
[P1] `file://` deeplink handling can panic on non-file URLs
On macOS, `url.to_file_path().unwrap()` will panic if the `file://` URL can’t be converted to a local path (e.g., malformed/UNC-style). Since deeplinks are external input, it’d be safer to return `Invalid`/`ParseFailed` instead of unwrapping here.
How can I resolve this? If you propose a fix, please make it concise. |
| @@ -0,0 +1,14 @@ | |||
| import { open, showHUD } from "@raycast/api"; | |||
There was a problem hiding this comment.
Only start-recording checks whether Cap is installed, but the README/summary claims this for all commands. Consider doing the same here (and the other commands) so open(cap-desktop://...) doesn’t end up falling back to the browser on machines without Cap installed.
| import { open, showHUD } from "@raycast/api"; | |
| import { open, showHUD, getApplications } from "@raycast/api"; | |
| export default async function Command() { | |
| try { | |
| const apps = await getApplications(); | |
| const capInstalled = apps.some( | |
| (app) => | |
| app.bundleId === "so.cap.desktop" || | |
| app.bundleId === "so.cap.desktop.dev", | |
| ); | |
| if (!capInstalled) { | |
| await showHUD("❌ Cap is not installed"); | |
| return; | |
| } | |
| const action = { stop_recording: null }; | |
| const deeplink = `cap-desktop://action?value=${encodeURIComponent(JSON.stringify(action))}`; | |
| await open(deeplink); | |
| await showHUD("⏹️ Recording stopped"); | |
| } catch (error) { | |
| console.error("Failed to stop recording:", error); | |
| await showHUD("❌ Failed to stop recording"); | |
| } | |
| } |
| "typescript": "^5.4.5" | ||
| }, | ||
| "scripts": { | ||
| "build": "ray build --skip-types -e dist -o dist", |
There was a problem hiding this comment.
Any reason to build with --skip-types? Feels like an easy way for a TS error to slip through.
| "build": "ray build --skip-types -e dist -o dist", | |
| "build": "ray build -e dist -o dist", |
| - **No devices available**: Returns user-friendly error message | ||
| - **Device disconnected**: Handles gracefully with appropriate feedback | ||
| - **Invalid action**: Logs and ignores malformed requests | ||
| - **App not ready**: Queues actions until app is ready |
There was a problem hiding this comment.
The guide mentions queuing actions when the app isn’t ready — I don’t think handle() does any queuing (it just spawns and executes). Might be worth tweaking this wording so it matches the actual behavior.
| eprintln!("Invalid deep link format \"{}\"", &url) | ||
| } | ||
| // Likely login action, not handled here. | ||
| _ => {} |
There was a problem hiding this comment.
_ => {} makes the following NotAction arm unreachable here (should trigger an unreachable-pattern warning). Probably just remove the catch-all and keep the explicit NotAction arm.
/claim #1540
Summary
Implements the \ bounty for deeplinks support + Raycast Extension as specified in #1540.
Changes
Deeplinks Added
Raycast Extension
Testing
Technical Details
Rust Changes:
Raycast Extension:
Ready for review! 🚀
Greptile Overview
Greptile Summary
This PR adds new Cap deeplink actions for recording control (pause/resume/toggle) and device management (set/switch camera/mic) in
apps/desktop/src-tauri/src/deeplink_actions.rs, and introduces a new Raycast extension underextensions/raycast/with seven no-view commands that invoke those deeplinks and provide HUD feedback.Main concern: the deeplink URL parsing/validation logic in
DeepLinkAction::try_fromappears inverted, causing validcap-desktop://action?...URLs to be treated asInvalid, which would prevent the Raycast commands (and any other deeplink client) from working at all.Minor follow-ups include making the Raycast “Cap installed” check consistent across commands, and avoiding non-reproducible
@latestusage in the extension’s publish script.Confidence Score: 2/5
DeepLinkAction::try_fromthat rejects validcap-desktop://actionURLs, which is core to the PR’s functionality. Other issues are smaller UX/reproducibility concerns.Important Files Changed
TryFrom<&Url>host validation appears inverted so validcap-desktop://actionURLs are rejected.start_recordingdeeplink with a Cap-installed check via bundle id.stop_recordingdeeplink and show HUD feedback.pause_recordingdeeplink and show HUD feedback.resume_recordingdeeplink and show HUD feedback.toggle_pause_recordingdeeplink and show HUD feedback.switch_cameradeeplink and show HUD feedback.switch_microphonedeeplink and show HUD feedback.@latest).Sequence Diagram
sequenceDiagram participant External as External App (Raycast) participant OS as OS URL Handler participant Cap as Cap Desktop (Tauri) participant DL as deeplink_actions.rs participant Rec as recording module participant Dev as device snapshot External->>External: Build action JSON External->>OS: open("cap-desktop://action?value=<encoded JSON>") OS->>Cap: Launch/activate app with URL(s) Cap->>DL: handle(app_handle, urls) DL->>DL: DeepLinkAction::try_from(url) alt parse OK DL->>DL: execute(action) alt recording control DL->>Rec: start/stop/pause/resume/toggle else device switching DL->>Dev: get_devices_snapshot() DL->>Cap: set_camera_input / set_mic_input end else parse error DL-->>Cap: log error and ignore end(2/5) Greptile learns from your feedback when you react with thumbs up/down!
Context used:
dashboard- CLAUDE.md (source)dashboard- AGENTS.md (source)